package com.lyu.xiaolvshu.note.biz.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.RandomUtil;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.base.Preconditions;
import com.lyu.framework.biz.context.holder.LoginUserContextHolder;
import com.lyu.framework.common.exception.BizException;
import com.lyu.framework.common.response.Response;
import com.lyu.framework.common.util.JsonUtils;
import com.lyu.xiaolvshu.note.biz.constant.MqConstants;
import com.lyu.xiaolvshu.note.biz.constant.RedisKeyConstants;
import com.lyu.xiaolvshu.note.biz.domain.dos.NoteDO;
import com.lyu.xiaolvshu.note.biz.domain.mapper.NoteDOMapper;
import com.lyu.xiaolvshu.note.biz.domain.mapper.TopicDOMapper;
import com.lyu.xiaolvshu.note.biz.enums.NoteStatusEnum;
import com.lyu.xiaolvshu.note.biz.enums.NoteTypeEnum;
import com.lyu.xiaolvshu.note.biz.enums.NoteVisibleEnum;
import com.lyu.xiaolvshu.note.biz.enums.ResponseCodeEnum;
import com.lyu.xiaolvshu.note.biz.model.vo.*;
import com.lyu.xiaolvshu.note.biz.rpc.DistributedIdGeneratorRpcService;
import com.lyu.xiaolvshu.note.biz.rpc.KeyValueRpcService;
import com.lyu.xiaolvshu.note.biz.rpc.UserRpcService;
import com.lyu.xiaolvshu.note.biz.service.NoteService;
import com.lyu.xiaolvshu.user.dto.resp.FindUserByIdRespDTO;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class NoteServiceImpl implements NoteService {

    @Resource
    private NoteDOMapper noteDOMapper;

    @Resource
    private TopicDOMapper topicDOMapper;

    @Resource
    private DistributedIdGeneratorRpcService distributedIdGeneratorRpcService;

    @Resource
    private KeyValueRpcService keyValueRpcService;

    @Resource
    private UserRpcService userRpcService;

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Resource
    private RocketMQTemplate rocketMQTemplate;

    @Resource(name = "taskExecutor")
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;

    private static final Cache<Long, String> LOCAL_CACHE = Caffeine.newBuilder()
            .initialCapacity(10000)
            .maximumSize(10000)
            .expireAfterWrite(1, TimeUnit.HOURS)
            .build();

    @Override
    public Response<?> publishNote(PublishNoteReqVO publishNoteReqVO) {
        Integer type = publishNoteReqVO.getType();

        NoteTypeEnum noteTypeEnum = NoteTypeEnum.valueOf(type);

        if (Objects.isNull(noteTypeEnum)) {
            throw new BizException(ResponseCodeEnum.NOTE_TYPE_ERROR);
        }

        String imgUris = null;
        Boolean isContentEmpty = true;
        String videoUri = null;

        switch (noteTypeEnum) {
            case IMAGE_TEXT -> {
                List<String> imgUriList = publishNoteReqVO.getImgUris();
                Preconditions.checkArgument(CollUtil.isNotEmpty(imgUriList), "笔记图片不能为空");
                Preconditions.checkArgument(imgUriList.size() <= 8, "笔记图片不能多于8张");

                imgUris = StringUtils.joinWith(",", imgUriList);
            }
            case VIDEO -> {
                videoUri = publishNoteReqVO.getVideoUri();
                Preconditions.checkArgument(StringUtils.isNotBlank(videoUri), "笔记视频不能为空");
            }
            default -> {

            }
        }

        String snowFlakeId = distributedIdGeneratorRpcService.getSnowFlakeId();
        String contentUuid = null;

        String content = publishNoteReqVO.getContent();

        if (StringUtils.isNotBlank(content)) {
            isContentEmpty = false;
            contentUuid = UUID.randomUUID().toString();

            boolean isSaveSuccess = keyValueRpcService.saveNoteContent(contentUuid, content);
            if (!isSaveSuccess) {
                throw new BizException(ResponseCodeEnum.NOTE_PUBLISH_FAIL);
            }
        }

        Long topicId = publishNoteReqVO.getTopicId();
        String topicName = null;

        if (Objects.nonNull(topicId)) {
            topicName = topicDOMapper.selectNameByPrimaryKey(topicId);
        }

        Long creatorId = LoginUserContextHolder.getUserId();

        NoteDO noteDO = NoteDO.builder()
                .id(Long.valueOf(snowFlakeId))
                .isContentEmpty(isContentEmpty)
                .imgUris(imgUris)
                .videoUri(videoUri)
                .creatorId(creatorId)
                .title(publishNoteReqVO.getTitle())
                .topicId(topicId)
                .topicName(topicName)
                .type(type)
                .visible(NoteVisibleEnum.PUBLIC.getCode())
                .createTime(LocalDateTime.now())
                .updateTime(LocalDateTime.now())
                .status(NoteStatusEnum.NORMAL.getCode())
                .isTop(Boolean.FALSE)
                .contentUuid(contentUuid)
                .build();

        try {
            noteDOMapper.insert(noteDO);
        } catch (Exception e) {
            log.error("==> 笔记存储失败", e);
            if (StringUtils.isNotBlank(contentUuid)) {
                keyValueRpcService.deleteNoteContent(contentUuid);
            }
            throw new BizException(ResponseCodeEnum.NOTE_PUBLISH_FAIL);
        }

        return Response.success();
    }

    @SneakyThrows
    @Override
    public Response<FindNoteDetailRespVO> findNoteDetail(FindNoteDetailReqVO findNoteDetailReqVO) {
        Long noteId = findNoteDetailReqVO.getId();
        Long userId = LoginUserContextHolder.getUserId();

        String noteDetailLocalCacheStr = LOCAL_CACHE.getIfPresent(noteId);
        if (StringUtils.isNotBlank(noteDetailLocalCacheStr)) {
            log.info("==> 命中了本地缓存；{}", noteDetailLocalCacheStr);
            FindNoteDetailRespVO noteDetailLocalCache = JsonUtils.parseObject(noteDetailLocalCacheStr, FindNoteDetailRespVO.class);
            checkNoteVisible(noteDetailLocalCache.getVisible(), userId, noteDetailLocalCache.getCreatorId());
            return Response.success(noteDetailLocalCache);
        }

        String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
        String noteDetailRedisJson = (String) redisTemplate.opsForValue().get(noteDetailRedisKey);
        if (StringUtils.isNotBlank(noteDetailRedisJson)) {
            FindNoteDetailRespVO noteDetailRedisVO = JsonUtils.parseObject(noteDetailRedisJson, FindNoteDetailRespVO.class);
            threadPoolTaskExecutor.submit(() -> {
                LOCAL_CACHE.put(noteId, Objects.isNull(noteDetailRedisVO) ? "null" : noteDetailRedisJson);
            });
            if (Objects.nonNull(noteDetailRedisVO)) {
                checkNoteVisible(noteDetailRedisVO.getVisible(), userId, noteDetailRedisVO.getCreatorId());
                return Response.success(noteDetailRedisVO);
            }
        }

        NoteDO noteDO = noteDOMapper.selectByPrimaryKey(noteId);

        if (Objects.isNull(noteDO)) {
            threadPoolTaskExecutor.submit(() -> {
                // 过期时间 60 秒
                long expireSeconds = 60;
                redisTemplate.opsForValue().set(noteDetailRedisKey, "null", expireSeconds, TimeUnit.SECONDS);
            });
            throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
        }

        Integer visible = noteDO.getVisible();
        checkNoteVisible(visible, userId, noteDO.getCreatorId());

        Long creatorId = noteDO.getCreatorId();
        CompletableFuture<FindUserByIdRespDTO> userResultFuture = CompletableFuture
                .supplyAsync(() -> userRpcService.findById(creatorId), threadPoolTaskExecutor);
        CompletableFuture<String> contentResultFuture = CompletableFuture.completedFuture(null);

        if (Objects.equals(noteDO.getIsContentEmpty(), Boolean.FALSE)) {
            contentResultFuture = CompletableFuture
                    .supplyAsync(() -> keyValueRpcService.findNoteContent(noteDO.getContentUuid()), threadPoolTaskExecutor);
        }

        CompletableFuture<String> finalContentResultFuture = contentResultFuture;
        CompletableFuture<FindNoteDetailRespVO> resultFuture = CompletableFuture
                .allOf(userResultFuture, contentResultFuture)
                .thenApply(s -> {
                    FindUserByIdRespDTO findUserByIdRespDTO = userResultFuture.join();
                    String content = finalContentResultFuture.join();

                    Integer type = noteDO.getType();
                    String imgUriStr = noteDO.getImgUris();
                    List<String> imgUris = null;
                    if (Objects.equals(type, NoteTypeEnum.IMAGE_TEXT.getCode()) && StringUtils.isNotBlank(imgUriStr)) {
                        imgUris = List.of(imgUriStr.split(","));
                    }

                    return FindNoteDetailRespVO.builder()
                            .id(noteDO.getId())
                            .type(noteDO.getType())
                            .title(noteDO.getTitle())
                            .content(content)
                            .imgUris(imgUris)
                            .topicId(noteDO.getTopicId())
                            .topicName(noteDO.getTopicName())
                            .creatorId(creatorId)
                            .creatorName(findUserByIdRespDTO.getNickName())
                            .avatar(findUserByIdRespDTO.getAvatar())
                            .videoUri(noteDO.getVideoUri())
                            .updateTime(noteDO.getUpdateTime())
                            .visible(visible)
                            .build();
                });

        FindNoteDetailRespVO findNoteDetailRespVO = resultFuture.get();
        threadPoolTaskExecutor.submit(() -> {
            // 过期时间（保底1天 + 随机秒数，将缓存过期时间打散，防止同一时间大量缓存失效，导致数据库压力太大）
            long expireSeconds = 60 * 60 * 24 + RandomUtil.randomInt(60 * 60 * 24);
            redisTemplate.opsForValue().set(noteDetailRedisKey, JsonUtils.toJsonString(findNoteDetailRespVO), expireSeconds, TimeUnit.SECONDS);
        });
        return Response.success(findNoteDetailRespVO);
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public Response<?> updateNote(UpdateNoteReqVO updateNoteReqVO) {
        Long noteId = updateNoteReqVO.getId();
        Integer type = updateNoteReqVO.getType();

        NoteTypeEnum noteTypeEnum = NoteTypeEnum.valueOf(type);

        if (Objects.isNull(noteTypeEnum)) {
            throw new BizException(ResponseCodeEnum.NOTE_TYPE_ERROR);
        }

        String imgsUris = null;
        String videoUri = null;
        switch (noteTypeEnum) {
            case IMAGE_TEXT -> {
                List<String> imgUriList = updateNoteReqVO.getImgUris();
                Preconditions.checkArgument(CollUtil.isNotEmpty(imgUriList), "笔记图片不能为空");
                Preconditions.checkArgument(imgUriList.size() <= 8, "笔记图片不能超过8张");
                imgsUris = StringUtils.joinWith(",", imgUriList);
            }
            case VIDEO -> {
                videoUri = updateNoteReqVO.getVideoUri();
                Preconditions.checkArgument(StringUtils.isNotBlank(videoUri), "笔记视频不能为空");
            }
            default -> {

            }
        }

        Long topicId = updateNoteReqVO.getTopicId();
        String topicName = null;
        if (Objects.nonNull(topicId)) {
            topicName = topicDOMapper.selectNameByPrimaryKey(topicId);
            if (StringUtils.isBlank(topicName)) {
                throw new BizException(ResponseCodeEnum.TOPIC_NOT_FOUND);
            }
        }
        // 延迟双删策略,保证数据一致性
        String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
        redisTemplate.delete(noteDetailRedisKey);

        String content = updateNoteReqVO.getContent();
        NoteDO noteDO = NoteDO.builder()
                .id(noteId)
                .isContentEmpty(StringUtils.isBlank(content))
                .imgUris(imgsUris)
                .videoUri(videoUri)
                .title(updateNoteReqVO.getTitle())
                .topicId(topicId)
                .topicName(topicName)
                .type(type)
                .updateTime(LocalDateTime.now())
                .build();
        noteDOMapper.updateByPrimaryKey(noteDO);

        // 一致性保证：延迟双删策略
        // 异步发送延时消息
        Message<String> message = MessageBuilder.withPayload(String.valueOf(noteId)).build();
        rocketMQTemplate.asyncSend(MqConstants.TOPIC_DELAY_DELETE_NOTE_REDIS_CACHE, message,
                new SendCallback() {
                    @Override
                    public void onSuccess(SendResult sendResult) {
                        log.info("## 延时删除 Redis 笔记缓存消息发送成功...");
                    }

                    @Override
                    public void onException(Throwable throwable) {
                        log.error("## 延时删除 Redis 笔记缓存消息发送失败...", throwable);
                    }
                }, 3000, 1);

        rocketMQTemplate.syncSend(MqConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE, noteId);
        log.info("====> MQ：删除笔记本地缓存发送成功...");

        String contentUuid = noteDOMapper.selectByPrimaryKey(noteId).getContentUuid();
        boolean isUpdateContentSuccess;
        if (StringUtils.isBlank(content)) {
            if (StringUtils.isNotBlank(contentUuid)) {
                isUpdateContentSuccess = keyValueRpcService.deleteNoteContent(contentUuid);
            } else {
                isUpdateContentSuccess = true;
            }
        } else {
            contentUuid = StringUtils.isBlank(contentUuid) ? UUID.randomUUID().toString() : contentUuid;
            isUpdateContentSuccess = keyValueRpcService.saveNoteContent(contentUuid, content);
        }
        if (!isUpdateContentSuccess) {
            throw new BizException(ResponseCodeEnum.NOTE_UPDATE_FAIL);
        }
        return Response.success();
    }

    @Override
    public void deleteNoteLocalCache(Long noteId) {
        LOCAL_CACHE.invalidate(noteId);
    }

    @Override
    public Response<?> deleteNote(DeleteNoteReqVO deleteNoteReqVO) {
        Long noteId = deleteNoteReqVO.getId();

        Long currentUser = LoginUserContextHolder.getUserId();

        NoteDO originNote = noteDOMapper.selectByPrimaryKey(noteId);

        if (Objects.isNull(originNote)) {
            throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
        }

        if (!Objects.equals(currentUser, originNote.getCreatorId())) {
            throw new BizException(ResponseCodeEnum.NOTE_CANT_OPERATE);
        }

        NoteDO noteDO = NoteDO.builder()
                .id(noteId)
                .status(NoteStatusEnum.DELETED.getCode())
                .updateTime(LocalDateTime.now())
                .build();
        int count = noteDOMapper.updateByPrimaryKeySelective(noteDO);
        if (count == 0) {
            throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
        }
        String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
        redisTemplate.delete(noteDetailRedisKey);

        rocketMQTemplate.syncSend(MqConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE, noteId);
        log.info("====> MQ：删除笔记本地缓存发送成功...");

        return Response.success();
    }

    @Override
    public Response<?> visibleOnlyMe(UpdateNoteVisibleOnlyMeReqVO updateNoteVisibleOnlyMeReqVO) {
        Long noteId = updateNoteVisibleOnlyMeReqVO.getId();

        Long currentUser = LoginUserContextHolder.getUserId();

        NoteDO originNote = noteDOMapper.selectByPrimaryKey(noteId);

        if (Objects.isNull(originNote)) {
            throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
        }

        if (!Objects.equals(currentUser, originNote.getCreatorId())) {
            throw new BizException(ResponseCodeEnum.NOTE_CANT_OPERATE);
        }

        NoteDO noteDO = NoteDO.builder()
                .id(noteId)
                .visible(NoteVisibleEnum.PRIVATE.getCode())
                .updateTime(LocalDateTime.now())
                .build();
        int count = noteDOMapper.updateVisibleOnlyMe(noteDO);
        if (count == 0) {
            throw new BizException(ResponseCodeEnum.NOTE_CANT_VISIBLE_ONLY_ME);
        }
        String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
        redisTemplate.delete(noteDetailRedisKey);

        rocketMQTemplate.syncSend(MqConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE, noteId);
        log.info("====> MQ：删除笔记本地缓存发送成功...");

        return Response.success();
    }

    @Override
    public Response<?> topNote(TopNoteReqVO topNoteReqVO) {
        Long noteId = topNoteReqVO.getId();

        Long currentUser = LoginUserContextHolder.getUserId();

        NoteDO originNote = noteDOMapper.selectByPrimaryKey(noteId);

        if (Objects.isNull(originNote)) {
            throw new BizException(ResponseCodeEnum.NOTE_NOT_FOUND);
        }

        if (!Objects.equals(currentUser, originNote.getCreatorId())) {
            throw new BizException(ResponseCodeEnum.NOTE_CANT_OPERATE);
        }

        NoteDO noteDO = NoteDO.builder()
                .id(noteId)
                .isTop(topNoteReqVO.getIsTop())
                .updateTime(LocalDateTime.now())
                .build();
        int count = noteDOMapper.updateIsTop(noteDO);
        if (count == 0) {
            throw new BizException(ResponseCodeEnum.NOTE_CANT_OPERATE);
        }
        String noteDetailRedisKey = RedisKeyConstants.buildNoteDetailKey(noteId);
        redisTemplate.delete(noteDetailRedisKey);

        rocketMQTemplate.syncSend(MqConstants.TOPIC_DELETE_NOTE_LOCAL_CACHE, noteId);
        log.info("====> MQ：删除笔记本地缓存发送成功...");

        return Response.success();
    }

    /**
     * 校验笔记是否可见
     *
     * @param visible    是否可见
     * @param currUserId 当前用户
     * @param creatorId  创建者
     */
    private void checkNoteVisible(Integer visible, Long currUserId, Long creatorId) {
        if (Objects.equals(visible, NoteVisibleEnum.PRIVATE.getCode()) && !Objects.equals(currUserId, creatorId)) {
            throw new BizException(ResponseCodeEnum.NOTE_PRIVATE);
        }
    }
}
